# -*- coding: utf-8 -*-
import sys
import random
import logging
import threading
from functools import partial

import Image, ImageDraw, ImageFilter
from DirectoryTools import getNewImage, expandDirs, chooseDir, flushWalls
from Effects import border, pageBend, shadow

class Monitor(object):
	"""
	A monitor and its extents
	"""
	def __init__(self, monitor, physical, working, flags, monitorNumber):
		self.monitorNumber = monitorNumber
		self.monitor = monitor
		self.physical = physical
		self.working = working
		self.blendColour = blendColour = 0
		
		self.size = self.getSize(*self.physical)
		self._size = self.size
		self.width, self.height = self.size
		self.left, self.top, self.right, self.bottom = self.physical
		self._left, self._top, self._right, self._bottom = self.physical
		
		self.isPrimary = (flags != 0)

		self.cTop = self.top
		if self.cTop < 0:
			self.cTop = 20000  + self.cTop
		self.cLeft = self.left
		if self.cLeft < 0:
			self.cLeft  = 20000  + self.cLeft

		self.wLeft = int(self.left)
		self.wTop = int(self.top)

		self.needVSplit = self.top < 0 < self.bottom
		self.needHSplit = self.left < 0 < self.right
		self.needsSplit = self.needVSplit or self.needHSplit
		self.imageList = set()
		self.bgColour = (0, 0, 0)
		self.Blending = False
		self.BlendRatio = 1.0
		self.Crop = False
		self.Fill = False
		self.Gradient = False
		self.PreRotate = False
		self.FillMode = 'Collage'
		self.StopThreshold = 64
		self.Spanning = False
		self.SingleFolderMode = False
		self.ResetCollage = False
		self.StackMode = False
		self.threads = []
		self.path = None

	def setBlendColour(self, blendColour):
		self.blendColour = blendColour
		
	def getSize(self, left, top, right, bottom):
		"""
		What's our actual width and height
		"""
		return (abs(right - left), abs(bottom - top))

	def __repr__(self):
		return ('extent: ' + str(self.physical) +
				' :: size: ' + str(self._size) +
				' :: primary: ' + str(self.isPrimary) +
				' :: needsSplit: ' + str(self.needsSplit) +
				' :: ' + str(self.monitorNumber))
	
	def __cmp__(self, other):
		"""
		So we can sort monitors, top to bottom, left to right
		"""
		if not cmp(self.cTop, other.cTop):
			return cmp(self.cLeft, other.cLeft)
		return cmp(self.top, other.top)

	def getMonitorDirs(self, config):
		"""
		Find the directories configured for this monitor

		:param config: a mod:`ConfigParser` object
		"""
		section = 'monitor_{0}'.format(self.monitorNumber)
		dirs = []
		# Check for [monitor_0]
		if config.has_section(section):
			# check for its own paths section
			if config.has_option(section, 'paths'):
				return config.get(section, 'paths').split('\n')
		return config.get('directories', 'paths').split('\n')

	def getConfigFloat(self, config, section, value, default):
		"""
		Convenience function for fetching floats with a default from a
		:mod:`ConfigParser` object
		"""
		try:
			return config.getfloat(section, value)
		except:
			pass
		return default
		
	def getConfigString(self, config, section, value, default):
		"""
		Convenience function for fetching strings with a default from a
		:mod:`ConfigParser` object
		"""
		try:
			return config.get(section, value).strip()
		except:
			pass
		return default		

	def getConfigBoolean(self, config, section, value, default):
		"""
		Convenience function for fetching booleans with a default from a
		:mod:`ConfigParser` object
		"""
		try:
			return config.getboolean(section, value)
		except:
			pass
		return default

	def setMonitorConfig(self, config):
		"""
		Configurate ourselvezz
		"""
		self.dirs = expandDirs(self.getMonitorDirs(config))

		if config.has_section('global'):
			cgb = partial(self.getConfigBoolean, config, 'global')
			cgf = partial(self.getConfigFloat, config, 'global')
			cgs = partial(self.getConfigString, config, 'global')
			
			self.Blending = cgb('Blending', False)
			self.BlendRatio = cgf('BlendRatio', 1.0)
			self.StopThreshold = cgf('StopThreshold', 64)
			self.Crop = cgb('Crop', False)
			self.Fill = cgb('Fill', False)
			self.Gradient = cgb('Gradient', False)
			self.PreRotate = cgb('PreRotate', False)
			self.FillMode = cgs('FillMode', 'Collage')
			self.BorderMode = cgb('BorderMode', False)
			self.SingleFolderMode = cgb('SingleFolderMode', False)
			self.Spanning = cgb('Spanning', False)
			self.ResetCollage = cgb('ResetCollageFolder', False)
			self.StackMode = cgb('StackMode', False)

		section = 'monitor_{0}'.format(self.monitorNumber)
		if config.has_section(section):
			cgb = partial(self.getConfigBoolean, config, section)
			cgf = partial(self.getConfigFloat, config, section)
			cgs = partial(self.getConfigString, config, section)
			
			self.Blending = cgb('Blending', self.Blending)
			self.BlendRatio = cgf('BlendRatio', self.BlendRatio)
			self.StopThreshold = cgf('StopThreshold', self.StopThreshold)
			self.Crop = cgb('Crop', self.Crop)
			self.Fill = cgb('Fill', self.Fill)
			self.Gradient = cgb('Gradient', self.Gradient)
			self.PreRotate = cgb('PreRotate', self.PreRotate)
			self.FillMode = cgs('FillMode', self.FillMode)
			self.BorderMode = cgb('BorderMode', self.BorderMode)
			self.SingleFolderMode = cgb('SingleFolderMode', self.SingleFolderMode)
			self.Spanning = cgb('Spanning', self.Spanning)
			self.ResetCollage = cgb('ResetCollageFolder', self.ResetCollage)
			self.StackMode = cgb('StackMode', self.StackMode)

		if self.StackMode:
			self.Blending = False

		if self.FillMode in ['Collage', 'Strip']:
			self.Fill = False
			self.PreRotate = False
			self.Crop = False

	def chooseDir(self, dirs):
		if (not self.SingleFolderMode) or (not self.path):
			self.path = chooseDir(dirs)
		return self.path

	def putImageAt(self, image, position, size, sizer):
		"""
		Resize and place the image on the background

		:param image: The current image
		:param position: Where to place the image
		:param size: The final size
		:param sizer: The resize method
		"""

		if self.StackMode and random.random() < 0.50:
			return
		
		image = image.convert("RGBA").resize(size, sizer)

		if self.BorderMode:
			image = border(image)

		if self.Blending:
			x, y = position
			w, h = size
			box = (x, y, x + w, y + h)
			img1 = self.bgImage.crop(box)
			image = Image.blend(img1, image, self.BlendRatio)

		self.bgImage.paste(image, position, mask = image)
		del image

	def placeImage(self, image):
		"""
		Find the scale and position for this image

		:param image: The image at hand
		"""
		
		scale, (imW, imH) = self._getMaxSize(image)
		w, h = self.size
		dX = w - imW
		dY = h - imH
		pos = (self.wLeft, self.wTop)

		# If there's space, choose whether to slide the image
		# left/right or up/down
		slide = random.randint(0, 1)

		if not slide:
			if max(dX, dY) == dX:
				self.wLeft += imW
			else:
				self.wTop += imH
		else:
			pos = (pos[0] + dX, pos[1] + dY)
			
		if max(dX, dY) == dX:
			self.size = (dX, h)
		else:
			self.size = (w, dY)

		rsFilter = Image.BICUBIC
		if scale < 1.0:
			rsFilter = Image.ANTIALIAS

		return pos, (imW, imH), rsFilter
			
	def makeStrips(self):
		"""
		Break up the monitor into sections
		"""
		# 1111111
		# -+---+-
		# 2|333|4
		# 2|333|4
		# -+---+-
		# 5555555
		strips = []
		w4 = int(self.width  * 0.18)
		h4 = int(self.height * 0.15)

		strips.append((
			self._left, self._top, self._right, h4))


		t = self._top + h4
		b = self._bottom - h4
		strips.append((
			self._left, t, self._left + w4, b))
		
		strips.append((
			self._left + w4, t , self._right - w4, b))

		strips.append((
			self._right - w4, t, self._right, b))

		strips.append((
			self._left, self._bottom - h4, self._right, self._bottom))
		
		return strips

	def buildSpiral(self):
		"""
		Break up the monitor into sections using the golden ratio
		"""
		ratio = 1.61803398875	
		sections = []
		left, top, right, bottom = (self._left, self._top, self._right, self._bottom)
		width = right - left
		height = bottom - top

		while width > self.StopThreshold and height > self.StopThreshold:
			val = int(width / ratio)
			sections.append((left, top, left + val, bottom))
			left = left + val
			width = right - left
			
			val = int(height / ratio)
			sections.append((left, top, right, top + val))
			top = top + val
			height = bottom - top
			
			val = int(width / ratio)			
			sections.append((right - val, top, right, bottom))
			right = right - val
			width = right - left
			
			val = int(height / ratio)			
			sections.append((left, bottom - val, right, bottom))
			bottom = bottom - val
			height = bottom - top
			
		if width > 0 and height > 0:	
			sections.append((left, top, right, bottom))
		
		return sections

	def buildStrips(self):
		if self.FillMode == 'Strip':
			strips = self.makeStrips()
		elif self.FillMode == 'Spiral':
			strips = self.buildSpiral()
			
		for strip in strips:
			self.left, self.top, self.right, self.bottom = strip
			self.width = self.right - self.left
			self.height = self.bottom - self.top
			self.size = (self.width, self.height)
			self.wLeft = self.left
			self.wTop = self.top
			self.buildCollage()
			# Reset to a new dir
			if self.ResetCollage:
				self.path = None

	def fillRegions(self, regions):
		for args in regions:
			t = threading.Thread(target = self.putImageAt,
								 args = args)
			self.threads.append(t)
			t.start()

	def waitForFill(self):
		for t in self.threads:
			t.join()

	def buildCollage(self):
		regions = []
		building = True
		if not self.dirs:
			return

		if len(self.dirs) == 1:
			# work around random
			self.dirs += self.dirs

		while building:
			lastSize = self.size
			lastPos = (self.wLeft, self.wTop)
			nDirs = len(self.dirs)
			pixels4 = (self.size[0] * self.size[1]) / 4
			image = None
			while nDirs and not image:
				path = self.chooseDir(self.dirs)
				if not path:
					nDirs = 0
					continue
				image = getNewImage(path, self.imageList)
				if not image:
					self.path = None
					self.imageList = set()
					continue
				
				iPix = image.size[0] * image.size[1]
				if iPix < pixels4:
					image = None
					self.path = None
					continue
				
			self.imageList.add(image.info['filename'])
			position, size, sizer = self.placeImage(image)
			regions.append((image, position, size, sizer))
			building = ( (self.size[0] > self.StopThreshold) and
						 (self.size[1] > self.StopThreshold))

		if self.size[0] > 0 or self.size[1] > 0:
			regions[-1] = (image, lastPos, lastSize, Image.ANTIALIAS)

		self.fillRegions(regions)
		self.waitForFill()
	
	def generateWallpaper(self, fillModes = ('Strip', 'Spiral')):
		if self.StackMode:
			def _blurBack(self):
				img = self.__bgImage.crop(self.physical).convert('RGBA').filter(ImageFilter.GaussianBlur(11))
				
				self.__bgImage.paste(img, (self._left, self._top))

			self._stackThread = threading.Thread(
				target = _blurBack, args = (self,))
			self._stackThread.start()
		try:
			if self.FillMode in fillModes:
				self.buildStrips()
				return

			if self.FillMode == 'Collage':
				self.buildCollage()
				return

			generated = False
			while not generated:
				generated = self.setWallpaperFromDirectory(self.chooseDir(self.dirs))
		finally:
			flushWalls()
			if self.StackMode:
				self._stackThread.join()
				self.__bgImage.paste(self.bgImage, (0, 0), mask = self.bgImage)
		
	def setWallpaperFromDirectory(self, path):
		"""
		Find an image, and set it into our wallpaper.

		:param path: A directory containing images
		"""
		image = getNewImage(path)
		if image:
			self.addWallpaper(image)
			return True
		return False

	def _getMaxSize(self, image, size = None):
		size = size or self.size
		
		if size == image.size:
			return 1.0, image.size

		scale = 2.0
		width, height = size
		imWidth, imHeight = image.size

		hScale = float(height) / float(imHeight)
		wScale = float(width) / float(imWidth)

		if self.Fill:
			scale = max(hScale, wScale)
		else:
			scale = min(hScale, wScale)
			
		newSize = (int(imWidth * scale), int(imHeight * scale))

		return scale, newSize
		
	def maxAspectWallpaper(self, image):
		"""
		Blow the image up as much as possible without exceeding the monitor
		bounds. If self.Fill is True then resize until the entire monitor is
		filled and crop the excess.

		:param image: A :mod:`Image.Image`
		"""
		rsFilter = Image.BICUBIC
		
		scale, newSize = self._getMaxSize(image)
		
		if scale > 1.0:
			rsFilter = Image.ANTIALIAS

		width, height = self.size

		if self.Fill:
			imWidth, imHeight = newSize
			x = int((imWidth - width) / 2.0)
			y = int((imHeight - height) / 2.0)
			bbox = (x, 0, width + x, height + y)
			image = image.crop(bbox)
		
		return image

	def setBgImage(self, bgImage):
		"""
		Set the global desktop image
		"""
		self.bgImage = bgImage

		if self.Blending:
			# Alpha blend the image with the current desktop colour
			# Or black if something goes wrong with getting the desktop colour
			self.bgColour = ((self.blendColour & 0xFF  ),
							 (self.blendColour & 0xFF00) >> 8,
							 (self.blendColour & 0xFF0000) >> 16)

			draw = ImageDraw.Draw(self.bgImage)
			draw.rectangle(self.physical, outline = self.bgColour,
						   fill = self.bgColour)
		
		if self.StackMode:
			self.__bgImage = self.bgImage
			self.bgImage = Image.new('RGBA', self.__bgImage.size, (0, 0, 0, 0))

	def centreImage(self, size):
		w, h = size
		mW, mH = self._size
		x = (mW - w) / 2
		y = (mH - h) / 2
		return (x, y)

	def addWallpaper(self, wallpaper):
		if isinstance(wallpaper, basestring):
			wallpaper = Image.load(wallpaper)

		if self.PreRotate:
			if wallpaper.size[0] < wallpaper.size[1]:
				wallpaper = wallpaper.rotate(90, Image.BICUBIC, expand = True)

		position, size, sizer = self.placeImage(wallpaper)
		position = self.centreImage(size)
		
		regions = [(wallpaper, position, size, sizer),]
			
		self.fillRegions(regions)
		self.waitForFill()
		
	def setWallpaper(self, wallpaper):
		"""
		Adds the wallpaper to the global bg wallpaper
		"""
		if not self.needsSplit:
			self.bgImage.paste(wallpaper, (self.wLeft, self.wTop))
			return

		bgImage = self.bgImage

		if self.needVSplit:
			height = -self.top
			bottom = wallpaper.crop((0, 0, self.width, height))
			bottom.load()
			top = wallpaper.crop((0, height, self.width, self.height))
			top.load()
			bgImage.paste(top, (self.wLeft, 0))
			bgImage.paste(bottom, (self.wLeft, bgImage.size[1] - height))
		else:
			width = -self.left
			right = wallpaper.crop((0, 0, width, self.height))
			right.load()
			left = wallpaper.crop((width, 0, self.width, self.height))
			left.load()
			bgImage.paste(left, (0, self.wTop))
			bgImage.paste(right, (bgImage.size[0] - width, self.wTop))

